package org.hawk.gwt.ppc.introspection.impl;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.hawk.gwt.ppc.introspection.NoSuchTypeException;
import org.hawk.gwt.ppc.introspection.SourcesRepository;
import org.hawk.gwt.ppc.loader.SourcesLoader;
import org.hawk.gwt.ppc.parser.ClassDescriptor;
import org.hawk.gwt.ppc.parser.CompilationUnitDescriptor;
import org.hawk.gwt.ppc.parser.JavaParser;
import org.hawk.gwt.ppc.parser.PreprocessorIOException;
import org.hawk.gwt.ppc.utils.IO;
import org.hawk.gwt.ppc.utils.Name;
import org.hawk.gwt.ppc.utils.PreprocessorException;

/**
 * Internal implementation of source repository for specific loader.
 * 
 * @author alex.bereznevatiy@gmail.com
 *
 */
final class SourceRepositoryImpl implements SourcesRepository {
	
	private Set<String> changedClasses;
	private Map<String, CompilationUnitDescriptor> compilationUnits;
	private Map<String, ClassDescriptor> classes;
	
	private SourcesLoader loader;

	SourceRepositoryImpl(SourcesLoader loader) {
		this.changedClasses = new HashSet<String>();
		this.compilationUnits = new HashMap<String, CompilationUnitDescriptor>();
		this.loader = loader;
		this.classes = new HashMap<String, ClassDescriptor>();
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.SourcesRepository#requireClass(java.lang.String)
	 */
	public ClassDescriptor requireClass(String className)
			throws PreprocessorException {
		ClassDescriptor clazz = findClass(className);
		if (clazz == null) {
			throw new NoSuchTypeException(className);
		}
		return clazz;
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.SourcesRepository#findClass(java.lang.String)
	 */
	public ClassDescriptor findClass(String className)
			throws PreprocessorException {
		ClassDescriptor clazz = this.classes.get(className);
		if (clazz != null) {
			return clazz;
		}
		String containerName = className;
		while (!loader.isExists(containerName)) {
			containerName = Name.getParentJavaName(containerName);
			if (containerName.length() == 0) {
				return null;
			}
		}
		
		CompilationUnitDescriptor cu = compilationUnits.get(containerName);
		if (cu != null) {
			return findFromCU(cu, className);
		}
		
		URL unit = loader.getCompilationUnit(containerName);
		if (unit != null) {
			cu = parseCU(containerName, unit);
			return findFromCU(cu, className);
		}
		
		loadWholePackage(containerName);
		
		// if its exists - the result will be in classes map
		return this.classes.get(className);
	}

	private ClassDescriptor findFromCU(CompilationUnitDescriptor cu, String className) {
		for (ClassDescriptor descriptor : cu.getTopClasses()) {
			ClassDescriptor result = findFromClass(descriptor, className);
			if (result != null) {
				return result;
			}
		}
		return null;
	}

	private ClassDescriptor findFromClass(ClassDescriptor descriptor, String className) {
		if (descriptor.getName().equals(className)) {
			return descriptor;
		}
		if (!className.startsWith(descriptor.getName()) ||
				className.charAt(descriptor.getName().length()) != Name.PACKAGE_SEPARATOR) {
			return null;
		}
		
		for (ClassDescriptor child : descriptor.getDeclaredClasses()) {
			ClassDescriptor result = findFromClass(child, className);
			if (result != null) {
				return result;
			}
		}
		return null;
	}

	private void loadWholePackage(String packageName) throws PreprocessorException {
		for (String cuName : loader.getCompilationUnits(packageName)) {
			findCompilationUnit(cuName);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.SourcesRepository#findCompilationUnit(java.lang.String)
	 */
	public CompilationUnitDescriptor findCompilationUnit(
			String compilationUnitName) throws PreprocessorException {
		CompilationUnitDescriptor result = compilationUnits.get(compilationUnitName);
		if (result != null) {
			return result;
		}
		URL unit = loader.getCompilationUnit(compilationUnitName);
		if (unit == null) {
			throw new NoSuchTypeException(compilationUnitName);
		}
		return parseCU(compilationUnitName, unit);
	}

	private CompilationUnitDescriptor parseCU(String name, URL unit)
			throws PreprocessorException {
		InputStream is = null;
		try {
			is = unit.openStream();
			JavaParser parser = JavaParser.create(is);
			CompilationUnitDescriptor result = parser.parse();
			registerCU(name, result);
			return result;
		} catch (IOException ex) {
			throw new PreprocessorIOException("Unable to parse compilation unit: " + name, ex);
		} catch (PreprocessorException ex) {
			throw new PreprocessorException("Unable to parse compilation unit: " + name, ex);
		} finally {
			IO.close(is);
		}
	}

	private void registerCU(String name, CompilationUnitDescriptor unit) {
		compilationUnits.put(name, unit);
		for (ClassDescriptor clazz : unit.getTopClasses()) {
			registerClass(clazz);
		}
	}

	private void registerClass(ClassDescriptor clazz) {
		classes.put(clazz.getName(), clazz);
		
		for (ClassDescriptor  child : clazz.getDeclaredClasses()) {
			registerClass(child);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.SourcesRepository#markChanged(java.lang.String)
	 */
	public void markChanged(String className) {
		changedClasses.add(className);
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.SourcesRepository#getChangedClasses()
	 */
	public Collection<String> getChangedClasses() {
		return Collections.unmodifiableSet(changedClasses);
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.SourcesRepository#isChanged(java.lang.String)
	 */
	public boolean isChanged(String clazz) {
		return changedClasses.contains(clazz);
	}

}
